package jam4j.util;

import java.lang.reflect.Field;
import java.util.*;

/**
 * A command-line parsing engine. It is configured by giving it an enum type
 * whose constants implement the {@link OptionSpec} interface and have
 * annotations giving salient properties.
 * 
 * @param <E> The enum type defining the options.
 * @param <S> The <em>state type</em> for the interpreter, to be manipulated by
 * the options and returned by {@link #parse(String...)}.
 * 
 * @author Luke Maurer
 */
public class OptionInterpreter<E extends Enum<E> & OptionSpec<E, S>, S> {
    private final Option[] options;
    private final Class<E> specClazz;
    
    private final Map<E, Option> optionMap;
    private final SortedMap<String, Option> longOptionMap;
    private final SortedMap<Character, Option> shortOptionMap;
    
    private S state;
    
    /**
     * Parse the given arguments according to the given enum type, returning
     * the final state of the interpreter.
     * 
     * @param <E> The enum type defining the options.
     * @param <S> The state type of the interpreter.
     * @param specClazz The Class object representing {@code E}.
     * @param args The arguments to parse.
     * @return The final state of the interpreter after all arguments are
     *         parsed.
     * @throws OptionSpec.OptionException If thrown by any of the enum
     *             constants implementing {@link OptionSpec}.
     */
    public static <E extends Enum<E> & OptionSpec<E, S>, S>
            S parse(Class<E> specClazz, String ... args)
                throws OptionSpec.OptionException {
        return new OptionInterpreter<E, S>(specClazz).parse(args);
    }
    
    /**
     * Parse the given arguments, returning the interpreter state afterward.
     * 
     * @param args The arguments to parse.
     * @return The state of the interpreter after the arguments are parsed.
     * @throws OptionSpec.OptionException If thrown by any of the enum
     *             constants implementing {@link OptionSpec}.
     */
    public S parse(String ... args)
            throws OptionSpec.OptionException {
        final int argc = args.length;
        for (int ix = 0; ix < argc; ix++) {
            final String arg = args[ix];
            
            if (arg.startsWith("--")) {
                final String optName, optArg;
                {
                    final int equalsIx = arg.indexOf('=');
                    if (equalsIx == -1) {
                        optName = arg.substring(2);
                        optArg = null;
                    } else {
                        optName = arg.substring(2, equalsIx);
                        optArg = arg.substring(equalsIx + 1);
                    }
                }
                
                final Option option = longOptionMap.get(optName);
                
                if (option == null)
                    throw new OptionSpec.OptionException(
                            "Option not recognized: --" + optName);
                
                if (option.argAllowed)
                    if (option.argRequired)
                        if (optArg != null)
                            option.spec.option(optArg, state);
                        else {
                            final String nextArg;
                            if (++ix == args.length ||
                                    (nextArg = args[ix]).startsWith("-"))
                                throw new OptionSpec.OptionException(
                                        "Option requires an argument: --" +
                                        optName);
                            else
                                option.spec.option(nextArg, state);
                        }
                    else
                        option.spec.option(optArg, state);
                else
                    if (optArg != null)
                        throw new OptionSpec.OptionException(
                                "Option does not take an argument: --" +
                                optName);
                    else
                        option.spec.option(null, state);
            } else if (arg.startsWith("-")) {
                final int len = arg.length();
                for (int charIx = 1; charIx < len; charIx++) {
                    final Option option =
                        shortOptionMap.get(arg.charAt(charIx));
                    
                    if (option == null)
                        throw new OptionSpec.OptionException(
                                "Option not recognized: -" + arg.charAt(ix));
                    
                    if (option.argAllowed) {
                        final String optArg;
                        {
                            final String rest = arg.substring(charIx + 1);
                            if (rest.isEmpty()) {
                                if (option.argRequired) {
                                    final String nextArg;
                                    if (++ix == args.length ||
                                            (nextArg = args[ix])
                                                .startsWith("-"))
                                        throw new OptionSpec.OptionException(
                                                "Option requires an argument: "
                                                + option.names[0]);
                                    else
                                        optArg = nextArg;
                                } else
                                    optArg = null;
                            } else
                                optArg = rest;
                        }
                        
                        option.option(optArg);
                        break;
                    } else
                        option.option();
                }
            } else
                argument(arg);
        }
        
        return state;
    }

    /**
     * Build an interpreter, configured by the given enum class.
     * 
     * @param specClazz The enum type configuring the new interpreter.
     * 
     * @throws IllegalArgumentException If {@code specClass} is empty.
     */
    @SuppressWarnings("unchecked")
    public OptionInterpreter(Class<E> specClazz) {
        this.specClazz = specClazz;
        this.optionMap = new EnumMap<E, Option>(specClazz);

        final SortedMap<String, Option> longOptionMap =
            new TreeMap<String, Option>();
        final SortedMap<Character, Option> shortOptionMap =
            new TreeMap<Character, Option>();
        
        this.longOptionMap = Collections.unmodifiableSortedMap(longOptionMap);
        this.shortOptionMap =
            Collections.unmodifiableSortedMap(shortOptionMap);
        
        final E[] specs = specClazz.getEnumConstants();
        this.options = new OptionInterpreter.Option[specs.length];
        
        if (specs.length == 0)
            throw new IllegalArgumentException("Empty enum: " + specClazz);
        
        for (E spec : specs) {
            final Option option = this.new Option(spec);
            optionMap.put(spec, option);
            
            for (String name : option.names)
                longOptionMap.put(name, option);
            for (char shortName : option.shortNames)
                shortOptionMap.put(shortName, option);
        }
        
        assert optionMap.size() == options.length;
        optionMap.values().toArray(options);
        
        state = options[0].spec.initialState();
    }
    
    /**
     * @return The enum type configuring this interpreter.
     */
    public Class<E> getSpecClass() {
        return specClazz;
    }
    
    private S argument(String value) throws OptionSpec.OptionException {
        return state = options[0].spec.argument(value, state);
    }
    
    private final class Option {
        final E spec;
        final Field field;
        final String[] names;
        final char[] shortNames;
        final String description, argName;
        final boolean argRequired, argAllowed, cumulative;
        boolean seen = false;
        
        S option(String argument) throws OptionSpec.OptionException {
            if (seen && !cumulative)
                throw new OptionSpec.RedundantOptionException(spec);
            seen = true;
            
            return state = spec.option(argument, state);
        }
        
        S option() throws OptionSpec.OptionException {
            return option(null);
        }
        
        Option(E spec) {
            this.spec = spec;
            final Field field = this.field = Enums.fieldForConstant(spec);
            
            {
                final OptionSpec.Name annot =
                    field.getAnnotation(OptionSpec.Name.class);
                this.names = annot == null ? 
                        new String[] { spec.name() } : annot.value();
            }
            
            {
                final OptionSpec.ShortName annot =
                    field.getAnnotation(OptionSpec.ShortName.class);
                this.shortNames = annot == null ?
                        new char[0] : annot.value();
            }
            
            {
                final OptionSpec.Description annot =
                    field.getAnnotation(OptionSpec.Description.class);
                this.description = annot == null ?
                        null : annot.value();
            }
            
            {
                final OptionSpec.Argument annot =
                    field.getAnnotation(OptionSpec.Argument.class);
                if (annot == null) {
                    argName = null;
                    argRequired = argAllowed = false;
                } else {
                    argAllowed = true;
                    argRequired = annot.required();
                    argName = annot.name();
                }
            }
            
            this.cumulative =
                field.isAnnotationPresent(OptionSpec.Cumulative.class);
            
            assert !this.argRequired || this.argAllowed;
        }
    }
}
